﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using System.ComponentModel;
using System.Security.Cryptography;

// The MBDB format specification and part of the following source code is 
// adapted from the project http://code.google.com/p/iphonebackupbrowser/.
// The project is licensed under the New BSD License.
// 
// For a description of the MBDB file format, see the following wiki:
// http://code.google.com/p/iphonebackupbrowser/wiki/MbdbMbdxFormat
// Note that the wiki is not always up-to-date; check the source code
// 'mdbd.cs' for more details.

namespace iPhoneBackupViewer
{
    public class MobileDatabase
    {
        public MobileFileInfo[] Records { get; private set; }

        public MobileDatabase(string filename)
        {
            using (Stream stream = new FileStream(filename, FileMode.Open, FileAccess.Read))
            using (MobileDatabaseReader reader = new MobileDatabaseReader(stream))
            {
                // Verify signature: "mbdb\5\0"
                if (BitConverter.ToString(reader.ReadSignature()) != "6D-62-64-62-05-00")
                    throw new InvalidDataException("Invalid MBDB format.");

                // Read records.
                List<MobileFileInfo> records = new List<MobileFileInfo>();
                while (stream.Position < stream.Length)
                {
                    MobileFileInfo r = reader.ReadRecord();
                    records.Add(r);
                }
                this.Records = records.ToArray();
            }
        }
    }

    [TypeConverter(typeof(ExpandableObjectConverter))]
    public class MobileFileInfo
    {
        [Description("Domain and namespace of the file, connected by a dash.")]
        public string DomainNamespace { get; set; }

        [Description("Full path of this file relative to the root of the namespace.")]
        public string Path { get; set; }

        [Description("Link target if this file is a symlink.")]
        public string LinkTarget { get; set; }

        public string DataHash { get; set; }         // SHA.1 for 'important' files

        public string Unknown1 { get; set; }

        [Description("Type and permissions of the file.")]
        public MobileFileMode Mode { get; set; }

        public bool IsDirectory
        {
            get { return Mode.Type == MobileFileType.Directory; }
        }

        public uint Unknown2 { get; set; }

        [Description("inode number.")]
        public uint IndexNode { get; set; }

        [Description("User ID of file owner.")]
        public uint UserId { get; set; }

        [Description("Group ID of file owner.")]
        public uint GroupId { get; set; }

        public DateTime LastWriteTime { get; set; }          // aTime or bTime is the former ModificationTime
        public DateTime LastAccessTime { get; set; }
        public DateTime CreationTime { get; set; }

        [Description("Size of the file, in bytes.")]
        public ulong Length { get; set; }

        public byte flag { get; set; }              // 0 for link, 4 for directory, otherwise values unknown (4 3 1)

        [Description("Number of properties associated with this file.")]
        public byte PropertyCount { get; set; }

        [Description("Properties associated with this file.")]
        public KeyValuePair<string, string>[] Properties { get; set; }

        private string key = null;

        /// <summary>
        /// Gets the name of the file in the backup directory. The name is
        /// the SHA1 signature of DomainNamespace + "-" + Path.
        /// </summary>
        [Description("Name of the file in the backup directory.")]
        public string Key
        {
            get
            {
                if (key == null)
                {
                    string data = DomainNamespace+"-"+Path;

                    using (SHA1 sha1 = new SHA1Managed())
                    {
                        byte[] hash = sha1.ComputeHash(Encoding.UTF8.GetBytes(data));
                        key = BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant();
                    }
                }
                return key;
            }
        }


        public string Domain
        {
            get
            {
                int k = DomainNamespace.IndexOf('-');
                return (k >= 0) ? DomainNamespace.Substring(0, k) : DomainNamespace;
            }
        }

        public string Namespace
        {
            get
            {
                int k = DomainNamespace.IndexOf('-');
                return (k >= 0) ? DomainNamespace.Substring(k + 1) : "";
            }
        }

        public override string ToString()
        {
            string path = this.Domain + "/" + this.Namespace;
            if (!path.EndsWith("/"))
                path += '/';
            path += this.Path;
            if (this.IsDirectory && !path.EndsWith("/"))
                path += '/';
            return path;
        }
    }

    [TypeConverter(typeof(ExpandableObjectConverter))]
    public struct MobileFileMode
    {
        public ushort Value;

        public MobileFileMode(ushort value)
        {
            this.Value = value;
        }

        public MobileFileType Type
        {
            get { return (MobileFileType)((Value >> 12) & 0xF); }
        }

        public bool IsSetUserId
        {
            get { return( Value & 0x0800)!=0; }
        }

        public bool IsSetGroupId
        {
            get { return (Value & 0x0400) != 0; }
        }

        public bool IsSticky
        {
            get { return (Value & 0x0200) != 0; }
        }

        public MobileFilePermission OwnerPermissions
        {
            get { return (MobileFilePermission)((Value >> 6) & 7); }
        }

        public MobileFilePermission GroupPermissions
        {
            get { return (MobileFilePermission)((Value >> 3) & 7); }
        }

        public MobileFilePermission OtherPermissions
        {
            get { return (MobileFilePermission)(Value & 7); }
        }

        /// <remarks>
        /// The format of the output is specified in
        /// http://pubs.opengroup.org/onlinepubs/009695399/utilities/ls.html
        /// </remarks>
        public override string ToString()
        {
            char sticky = '-';
            if (this.IsSetUserId)
                sticky = (this.OwnerPermissions & MobileFilePermission.Execute) != 0 ? 's' : 'S';
            else if (this.IsSetGroupId)
                sticky = (this.GroupPermissions & MobileFilePermission.Execute) != 0 ? 's' : 'S';
            else if (this.IsSticky && this.Type == MobileFileType.Directory)
                sticky = (OtherPermissions & MobileFilePermission.Execute) != 0 ? 't' : 'T';
            else if ((this.Value & (1 + 8 + 64)) != 0)
                sticky = 'x';
            else
                sticky = '-';

            return string.Format("{0}{1}{2}{3}{4}",
                FormatType(Type),
                FormatPermission(OwnerPermissions),
                FormatPermission(GroupPermissions),
                FormatPermission(OtherPermissions),
                sticky);
        }

        private static string FormatPermission(MobileFilePermission perm)
        {
            return string.Format("{0}{1}{2}",
                (perm & MobileFilePermission.Read) != 0 ? 'r' : '-',
                (perm & MobileFilePermission.Write) != 0 ? 'w' : '-',
                (perm & MobileFilePermission.Execute) != 0 ? 'x' : '-');
        }

        private static char FormatType(MobileFileType type)
        {
            switch (type)
            {
                case MobileFileType.Pipe: return 'p';
                case MobileFileType.Character: return 'c';
                case MobileFileType.Directory: return 'd';
                case MobileFileType.Block: return 'b';
                case MobileFileType.Regular: return '-';
                case MobileFileType.SymLink: return 'l';
                case MobileFileType.Socket: return 's';
                default: return '?';
            }
        }
    }

    [Flags]
    public enum MobileFilePermission
    {
        None = 0,
        Read = 4,
        Write = 2,
        Execute = 1,
    }

    public enum MobileFileType
    {
        Pipe = 1,
        Character = 2,
        Directory = 4,
        Block = 6,
        Regular = 8,
        SymLink = 10,
        Socket = 12,
    }

    internal class MobileDatabaseReader : Util.IO.BigEndianBinaryReader
    {
        // Internal buffer for reading strings. This is not thread safe.
        private byte[] buffer = new byte[65535];

        private static DateTime unixEpoch = new DateTime(1970, 1, 1, 0, 0, 0);

        public MobileDatabaseReader(Stream stream)
            : base(stream) { }

        public virtual void ReadFull(byte[] buffer, int index, int count)
        {
            while (count > 0)
            {
                int actual = Read(buffer, index, count);
                if (actual == 0)
                    throw new EndOfStreamException();
                index += actual;
                count -= actual;
            }
        }

        public override string ReadString()
        {
            ushort len = ReadUInt16();
            if (len == 0xFFFF)
                return null;
            ReadFull(buffer, 0, len);
            return Encoding.UTF8.GetString(buffer, 0, len);
        }

        public byte[] ReadSignature()
        {
            byte[] data = new byte[6];
            ReadFull(data, 0, 6);
            return data;
        }

        public DateTime ReadDateTime()
        {
            return unixEpoch.AddSeconds(ReadUInt32());
        }

        public MobileFileInfo ReadRecord()
        {
            MobileFileInfo r = new MobileFileInfo();
            r.DomainNamespace = ReadString();

            r.Path = ReadString();
            r.LinkTarget = ReadString();
            r.DataHash = ReadString();
            r.Unknown1 = ReadString();

            r.Mode = new MobileFileMode(ReadUInt16());
            r.Unknown2 = ReadUInt32();
            r.IndexNode = ReadUInt32();
            r.UserId = ReadUInt32();
            r.GroupId = ReadUInt32();

            r.LastWriteTime = ReadDateTime();
            r.LastAccessTime = ReadDateTime();
            r.CreationTime = ReadDateTime();

            r.Length = ReadUInt64();
            r.flag = ReadByte();
            r.PropertyCount = ReadByte();

            if (r.PropertyCount > 0)
            {
                r.Properties = new KeyValuePair<string, string>[r.PropertyCount];
                for (int i = 0; i < r.PropertyCount; i++)
                {
                    string key = ReadString();
                    string value = ReadString();
                    r.Properties[i] = new KeyValuePair<string, string>(key, value);
                }
            }
            return r;
        }
    }
}
